﻿namespace Okazuki.MVVM.PrismSupport.Utils
{
    using System;
    using System.ComponentModel;
    using System.Linq;
    using System.Collections.Generic;
    using System.Linq.Expressions;
    using System.Reflection;

    public static class ObjectUtils
    {
#if !SILVERLIGHT
        /// <summary>
        /// 初期化処理のキャッシュ
        /// </summary>
        private static Dictionary<Type, Action<object>> InitActionCache = new Dictionary<Type, Action<object>>();

        private static MethodInfo NewGuidMethod = typeof(Guid).GetMethod("NewGuid");

        private static MethodInfo ToStringMethod = typeof(object).GetMethod("ToString");
#endif

#if !SILVERLIGHT
        /// <summary>
        /// プロパティのコピーを行います。
        /// </summary>
        /// <param name="source">元になるオブジェクト</param>
        /// <param name="target">コピー先のオブジェクト</param>
        public static void CopyTo(object source, object target)
        {
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            if (target == null)
            {
                throw new ArgumentNullException("target");
            }

            var propPair =
                from sourceProperty in source.GetType().GetProperties()
                from targetProperty in target.GetType().GetProperties()
                where sourceProperty.Name == targetProperty.Name
                where sourceProperty.CanRead && targetProperty.CanWrite
                select new { sourceProperty, targetProperty };

            var propertyDescriptors = TypeDescriptor.GetProperties(target);

            foreach (var pair in propPair)
            {
                var desc = propertyDescriptors.Find(pair.targetProperty.Name, false);
                var sourceValue = pair.sourceProperty.GetValue(source, null);
                object value = null;
                if (sourceValue == null)
                {
                    if (pair.targetProperty.PropertyType.IsValueType)
                    {
                        value = Activator.CreateInstance(pair.targetProperty.PropertyType);
                    }
                }
                else if (pair.sourceProperty.PropertyType == pair.targetProperty.PropertyType)
                {
                    value = sourceValue;
                }
                else
                {
                    value = desc == null ?
                        Convert.ChangeType(sourceValue, pair.targetProperty.PropertyType) :
                        desc.Converter.ConvertTo(sourceValue, pair.targetProperty.PropertyType);
                }
                pair.targetProperty.SetValue(
                    target,
                    value,
                    null);
            }
        }
#else
        /// <summary>
        /// プロパティのコピーを行います。
        /// </summary>
        /// <param name="source">元になるオブジェクト</param>
        /// <param name="target">コピー先のオブジェクト</param>
        public static void CopyTo(object source, object target)
        {
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            if (target == null)
            {
                throw new ArgumentNullException("target");
            }

            var propPair =
                from sourceProperty in source.GetType().GetProperties()
                from targetProperty in target.GetType().GetProperties()
                where sourceProperty.Name == targetProperty.Name
                where sourceProperty.CanRead && targetProperty.CanWrite
                select new { sourceProperty, targetProperty };

            foreach (var pair in propPair)
            {
                var sourceValue = pair.sourceProperty.GetValue(source, null);
                object value = null;
                if (sourceValue == null)
                {
                    if (pair.targetProperty.PropertyType.IsValueType)
                    {
                        value = Activator.CreateInstance(pair.targetProperty.PropertyType);
                    }
                }
                else if (pair.sourceProperty.PropertyType == pair.targetProperty.PropertyType)
                {
                    value = sourceValue;
                }
                else
                {
                    value = Convert.ChangeType(sourceValue, pair.targetProperty.PropertyType, null);
                }
                pair.targetProperty.SetValue(
                    target,
                    value,
                    null);
            }
        }
#endif

#if SILVERLIGHT
        /// <summary>
        /// AutoInit属性のついているプロパティを初期化します
        /// </summary>
        /// <param name="obj">初期化対象のオブジェクト</param>
        public static void Init(object obj)
        {
            if (obj == null)
            {
                throw new ArgumentNullException("obj");
            }

            var targetProperties = obj.GetType().GetProperties()
                .Where(p => p.GetCustomAttributes(typeof(AutoInitAttribute), true).Any());
            foreach (var prop in targetProperties)
            {
                prop.SetValue(
                    obj,
                    Activator.CreateInstance(prop.PropertyType),
                    null);
            }
        }
#else
        /// <summary>
        /// AutoInit属性のついているプロパティを初期化します
        /// </summary>
        /// <param name="obj">初期化対象のオブジェクト</param>
        public static void Init(object obj)
        {
            if (obj == null)
            {
                throw new ArgumentNullException("obj");
            }

            if (InitActionCache.ContainsKey(obj.GetType()))
            {
                InitActionCache[obj.GetType()](obj);
                return;
            }

            lock (InitActionCache)
            {
                if (InitActionCache.ContainsKey(obj.GetType()))
                {
                    InitActionCache[obj.GetType()](obj);
                    return;
                }

                CreateAndRegistInitAction(obj.GetType())(obj);
            }
        }

        private static Action<object> CreateAndRegistInitAction(Type type)
        {
            var parameter = Expression.Parameter(typeof(object), "obj");
            var cast = Expression.Convert(parameter, type);

            var targetProperties = type.GetProperties()
                .Where(p => p.GetCustomAttributes(typeof(AutoInitAttribute), true).Any())
                .Select(p =>
                    Expression.Assign(
                        Expression.Property(cast, p),
                        Expression.New(p.PropertyType)));

            var guidProperties = type.GetProperties()
                .Where(p => p.GetCustomAttributes(typeof(GuidAttribute), true).Any())
                .Select(p =>
                    Expression.Assign(
                        Expression.Property(cast, p),
                        Expression.Call(
                            Expression.Call(
                                NewGuidMethod),
                            ToStringMethod)));

            if (!targetProperties.Any() && !guidProperties.Any())
            {
                InitActionCache[type] = EmptyInitAction;
                return EmptyInitAction;
            }

            var action = Expression.Lambda<Action<object>>(
                Expression.Block(targetProperties.Concat(guidProperties)),
                parameter).Compile();
            InitActionCache[type] = action;
            return action;
        }

        private static void EmptyInitAction(object obj)
        {
        }
#endif
    }
}
